package gae.testjig.server;

import java.util.ConcurrentModificationException;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;

public class ConstraintTest {

	static private Key seatsRootKey;
	static {
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		Entity rootEntity = new Entity("SeatsRoot", "seats_root_key_name"); // we want only one of these entities to ever exist
		ds.put(rootEntity);
		seatsRootKey = rootEntity.getKey();
	}

	private DatastoreService datastore;
	private Transaction txn;
	private Key seatKey;

	public String beginTransaction() {
		datastore = DatastoreServiceFactory.getDatastoreService();
		txn = datastore.beginTransaction();
		return txn.getId();
	}
	
	public boolean testExists(String seatId) {
		// this search has to be an "ancestor query". (I think by naming an ancestor, it gives a scope for the "lock", which
		// is not a lock at all, but a metadata timestamp in the specified ancestor, which will help determine any "fault"
		// during our optimistic writing efforts.)
		
		Query testExistsQuery = new Query("Seat");
		testExistsQuery.setAncestor(seatsRootKey);
		testExistsQuery.addFilter("seatId", Query.FilterOperator.EQUAL, seatId);

		Entity exists = datastore.prepare(txn, testExistsQuery).asSingleEntity();
		if ( exists == null ) {
			return false;
		} else {
			return true;
		}
	}
	
	public String createSeatEntity(String seatId, String ownerName) {
		Entity seatEntity = new Entity("Seat", seatId, seatsRootKey);
		seatEntity.setProperty("ownerName", ownerName);
		seatEntity.setProperty("seatId", seatId);
		seatEntity.setProperty("timeStamp", System.currentTimeMillis());
		datastore.put(txn, seatEntity);
		seatKey = seatEntity.getKey();
		return KeyFactory.keyToString(seatKey);
	}
	
	public void deleteSeatEntity(String seatId) {
		Query testExistsQuery = new Query("Seat");
		testExistsQuery.setAncestor(seatsRootKey);
		testExistsQuery.addFilter("seatId", Query.FilterOperator.EQUAL, seatId);
		
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		Entity exists = ds.prepare(testExistsQuery).asSingleEntity();
		if ( null != exists ) {
			ds.delete(exists.getKey());
		}
	}
	
	public String fetchSeatInfo(String seatId) {
		Query testExistsQuery = new Query("Seat");
//		testExistsQuery.setAncestor(seatsRootKey);
		testExistsQuery.addFilter("seatId", Query.FilterOperator.EQUAL, seatId);
		
		DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
		Entity seat = ds.prepare(testExistsQuery).asSingleEntity();
		if ( null != seat ) {
			String ownerName = (String) seat.getProperty("ownerName");
			Long timestamp = (Long) seat.getProperty("timeStamp");
			return "Found seat entity: seatId="+seatId+", ownerName="+ownerName+", timestamp="+timestamp;
		} else {
			return "There is no entity in the datastore with seatId="+seatId;
		}
	}

	public void endTransaction() throws CommitException {
		try {
			txn.commit();
		}
		catch ( Exception e ) {
			throw new CommitException(e);
		}
		finally {
			if (txn.isActive()) {
		        txn.rollback();
		    }
		}	
	}
	
	
	/*
	 * This is a standalone function that does all the steps as we normally would do, without
	 * breaking them up into incremental steps per functino call. This is the real-time
	 * version.
	 */
	public void reserveSeat(ChannelLogger logger, String ownerName, String seatId) throws DuplicateException {
		logger.info(ownerName+": BEGIN");
		for (int i=0;i<10;i++) {
			try {
				reserveSeatAttempt(logger, ownerName, seatId);
				logger.info(ownerName+": END");
				return; // we get here if reservation succeeds
			}
			catch (ConcurrentModificationException cme) {
				logger.info(ownerName+": EXCEPTION java.util.ConcurrentModificationException");
				// stay in the loop and try again.
			}
			// you could use another backoff algorithm here rather than 100ms each time.
			try { Thread.sleep(100); } catch (InterruptedException e) {}
		}
		logger.info(ownerName+": ABORT");
		throw new ConcurrentModificationException("failed to reserve seat "+seatId);
	}

	private void reserveSeatAttempt(ChannelLogger logger, String ownerName, String seatId) throws DuplicateException, ConcurrentModificationException {
		logger.info(ownerName+": beginning transaction.");
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		Transaction txn = datastore.beginTransaction();
		logger.info(ownerName+": transactionId="+txn.getId());

		try {
			logger.info(ownerName+": test for existence of entity with seatId=" + seatId);
			Query testExistsQuery = new Query("Seat");
			testExistsQuery.setAncestor(seatsRootKey);
			testExistsQuery.addFilter("seatId", Query.FilterOperator.EQUAL, seatId);
	
			Entity exists = datastore.prepare(txn, testExistsQuery).asSingleEntity();
			if ( exists != null ) {
				logger.info(ownerName+": sorry, that seat has already been taken.");
				throw new DuplicateException("seatId "+seatId+" already exists.");
			} else {
				logger.info(ownerName+": that seat has not been taken yet.");
				logger.info(ownerName+": create the seat entity with seatId=" + seatId);
				Entity seatEntity = new Entity("Seat", seatId, seatsRootKey);
				seatEntity.setProperty("ownerName", ownerName);
				seatEntity.setProperty("seatId", seatId);
				seatEntity.setProperty("timeStamp", System.currentTimeMillis());
				datastore.put(txn, seatEntity);
				logger.info(ownerName+": created seat. entity key = " + KeyFactory.keyToString(seatEntity.getKey()));
				txn.commit(); // throws java.util.ConcurrentModificationException if entity group was modified by other thread
				logger.info(ownerName+": transaction committed.");
			}
		}
		finally {
			if (txn.isActive()) {
		        txn.rollback();
		    }
		}
	}

}
